| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944 |
- "use client";
- import { Link } from "@/i18n/navigation";
- import { useEffect, useMemo, useState } from "react";
- import { fetchWalletBalance } from "@/lib/account-api";
- import {
- fetchSavedWithdrawAccounts,
- fetchWithdrawBankOptions,
- fetchWithdrawChannels,
- submitWithdrawApply,
- type SavedWithdrawAccount,
- type WithdrawBankOption,
- type WithdrawChannel,
- } from "@/lib/withdrawal-api";
- import { InlineLoading } from "@/components/ui/loading-state";
- import { ModalShell } from "@/components/ui/modal-shell";
- function channelGroupLabel(channel: WithdrawChannel): string {
- const type = channel.type;
- const code = (channel.code || "").toUpperCase();
- const name = `${channel.name || ""} ${channel.enName || ""}`.toUpperCase();
- const aliHint = code.includes("ALI") || code.includes("ALIPAY") || name.includes("ALIPAY");
- if (type === "BANK_TELEGRAPHIC") return "国际转账";
- if (type === "BANK") return "网银支付";
- if (type === "DIGITAL_CURRENCY") return "数字货币";
- if (type === "CHANNEL_TYPE_WALLET") return "电子钱包";
- if (type === "CHANNEL_TYPE_CARD") return "信用卡";
- if (type === "CHANNEL_TYPE_ALI_WALLET" || aliHint) return "支付宝";
- if (type === "UCARD_WALLET") return "电子卡";
- return "其他";
- }
- function groupOrder(label: string): number {
- if (label === "数字货币") return 1;
- if (label === "网银支付") return 2;
- if (label === "国际转账") return 3;
- if (label === "电子钱包") return 4;
- if (label === "电子卡") return 5;
- if (label === "支付宝") return 6;
- return 99;
- }
- function formatAmountRange(item: WithdrawChannel): string {
- const min = item.minAmount || 0;
- const max = item.maxAmount > 0 ? item.maxAmount : "-";
- return `$${min} - $${max} ${item.currency || "USD"}`;
- }
- function formatFee(item: WithdrawChannel): string {
- if (item.feeType === 1) return `${item.free ?? 0}%`;
- if (item.feeType === 2) return `$${item.feeAmount ?? 0}`;
- if (item.free !== null && item.free !== undefined) return `${item.free}%`;
- return "0%";
- }
- function sanitizeHtml(input: string): string {
- if (!input) return "";
- return input
- .replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, "")
- .replace(/\son\w+="[^"]*"/gi, "")
- .replace(/\son\w+='[^']*'/gi, "");
- }
- function isWalletType(type: string): boolean {
- return type === "CHANNEL_TYPE_WALLET" || type === "CHANNEL_TYPE_ALI_WALLET";
- }
- function isBankType(type: string): boolean {
- return type === "BANK";
- }
- function isCardType(type: string): boolean {
- return type === "CHANNEL_TYPE_CARD";
- }
- function needsSavedAccount(type: string): boolean {
- return (
- type === "BANK" ||
- type === "BANK_TELEGRAPHIC" ||
- type === "CHANNEL_TYPE_CARD" ||
- type === "DIGITAL_CURRENCY"
- );
- }
- function savedAccountType(type: string): number | null {
- if (type === "BANK") return 1;
- if (type === "BANK_TELEGRAPHIC") return 2;
- if (type === "CHANNEL_TYPE_CARD") return 3;
- if (type === "DIGITAL_CURRENCY") return 4;
- return null;
- }
- export default function WithdrawApplyPage() {
- const [channels, setChannels] = useState<WithdrawChannel[]>([]);
- const [channelsLoading, setChannelsLoading] = useState(false);
- const [channelsError, setChannelsError] = useState<string | null>(null);
- const [savedAccountsError, setSavedAccountsError] = useState<string | null>(null);
- const [walletBalance, setWalletBalance] = useState<number | null>(null);
- const [walletBalanceLoading, setWalletBalanceLoading] = useState(false);
- const [savedAccounts, setSavedAccounts] = useState<SavedWithdrawAccount[]>([]);
- const [bankOptions, setBankOptions] = useState<WithdrawBankOption[]>([]);
- const [selectedChannelId, setSelectedChannelId] = useState("");
- const [selectedSavedId, setSelectedSavedId] = useState("");
- const [selectedBankCode, setSelectedBankCode] = useState("");
- const [address, setAddress] = useState("");
- const [amount, setAmount] = useState("");
- const [agree, setAgree] = useState(false);
- const [agreeExtra, setAgreeExtra] = useState(false);
- const [agencyNo, setAgencyNo] = useState("");
- const [cpf, setCpf] = useState("");
- const [bankUnameInput, setBankUnameInput] = useState("");
- const [bankCardNumInput, setBankCardNumInput] = useState("");
- const [bankNameInput, setBankNameInput] = useState("");
- const [bankBranchNameInput, setBankBranchNameInput] = useState("");
- const [swiftCodeInput, setSwiftCodeInput] = useState("");
- const [customBankCodeInput, setCustomBankCodeInput] = useState("");
- const [bankAddrInput, setBankAddrInput] = useState("");
- const [telegraphicCurrency, setTelegraphicCurrency] = useState("USD");
- const [cardUnameInput, setCardUnameInput] = useState("");
- const [cardNumInput, setCardNumInput] = useState("");
- const [cardCvvInput, setCardCvvInput] = useState("");
- const [cardExpiryInput, setCardExpiryInput] = useState("");
- const [submitting, setSubmitting] = useState(false);
- const [confirmOpen, setConfirmOpen] = useState(false);
- const [expandedGroup, setExpandedGroup] = useState<string>("数字货币");
- const [applyDialogOpen, setApplyDialogOpen] = useState(false);
- const [resultDialog, setResultDialog] = useState<{
- open: boolean;
- status: "success" | "error";
- title: string;
- message: string;
- }>({
- open: false,
- status: "success",
- title: "",
- message: "",
- });
- const selectedChannel = useMemo(
- () => channels.find((item) => item.id === selectedChannelId) ?? null,
- [channels, selectedChannelId],
- );
- const selectedSavedAccount = useMemo(
- () => savedAccounts.find((item) => item.id === selectedSavedId) ?? null,
- [savedAccounts, selectedSavedId],
- );
- const filteredSavedAccounts = useMemo(() => {
- if (!selectedChannel) return [];
- const type = savedAccountType(selectedChannel.type);
- if (type === null) return [];
- return savedAccounts.filter((item) => item.type === type);
- }, [savedAccounts, selectedChannel]);
- const shouldRequireSavedAccount = Boolean(selectedChannel && needsSavedAccount(selectedChannel.type));
- const shouldShowSavedAccountSelector = shouldRequireSavedAccount && filteredSavedAccounts.length > 0;
- const isBankTelegraphic = selectedChannel?.type === "BANK_TELEGRAPHIC";
- const needCpf = selectedChannel?.code === "PAY_RETAILER_REMIT_PAY_KEY_BRW";
- function resetApplyForm() {
- setSelectedSavedId("");
- setSelectedBankCode("");
- setAddress("");
- setAmount("");
- setAgree(false);
- setAgreeExtra(false);
- setAgencyNo("");
- setCpf("");
- setBankUnameInput("");
- setBankCardNumInput("");
- setBankNameInput("");
- setBankBranchNameInput("");
- setSwiftCodeInput("");
- setCustomBankCodeInput("");
- setBankAddrInput("");
- setTelegraphicCurrency("USD");
- setCardUnameInput("");
- setCardNumInput("");
- setCardCvvInput("");
- setCardExpiryInput("");
- }
- useEffect(() => {
- let cancelled = false;
- async function loadBase() {
- setWalletBalanceLoading(true);
- setChannelsLoading(true);
- setChannelsError(null);
- try {
- const [balanceResult, channelsResult] = await Promise.all([
- fetchWalletBalance(),
- fetchWithdrawChannels(),
- ]);
- if (cancelled) return;
- setWalletBalance(balanceResult);
- setChannels(channelsResult);
- } catch (e) {
- if (cancelled) return;
- const err = e as Error;
- setChannelsError(err?.message || "提款通道加载失败");
- setChannels([]);
- setWalletBalance(null);
- } finally {
- if (!cancelled) {
- setWalletBalanceLoading(false);
- setChannelsLoading(false);
- }
- }
- }
- void loadBase();
- return () => {
- cancelled = true;
- };
- }, []);
- useEffect(() => {
- if (!applyDialogOpen) return;
- let cancelled = false;
- async function loadSavedAccounts() {
- setSavedAccountsError(null);
- try {
- const list = await fetchSavedWithdrawAccounts();
- if (cancelled) return;
- setSavedAccounts(list);
- } catch (e) {
- if (cancelled) return;
- const err = e as Error;
- setSavedAccountsError(err?.message || "收款信息加载失败");
- setSavedAccounts([]);
- }
- }
- void loadSavedAccounts();
- return () => {
- cancelled = true;
- };
- }, [applyDialogOpen]);
- useEffect(() => {
- if (!selectedChannel || !applyDialogOpen) {
- setBankOptions([]);
- setSelectedBankCode("");
- return;
- }
- const currentChannel = selectedChannel;
- setSelectedSavedId("");
- setAddress("");
- setAgree(false);
- setAgreeExtra(false);
- setBankUnameInput("");
- setBankCardNumInput("");
- setBankNameInput("");
- setBankBranchNameInput("");
- setSwiftCodeInput("");
- setCustomBankCodeInput("");
- setBankAddrInput("");
- setTelegraphicCurrency("USD");
- setCardUnameInput("");
- setCardNumInput("");
- setCardCvvInput("");
- setCardExpiryInput("");
- if (!currentChannel.bankValid) {
- setBankOptions([]);
- setSelectedBankCode("");
- return;
- }
- let cancelled = false;
- async function loadBankOptions() {
- try {
- const list = await fetchWithdrawBankOptions(currentChannel.code);
- if (cancelled) return;
- setBankOptions(list);
- setSelectedBankCode((prev) => prev || list[0]?.code || "");
- } catch {
- if (cancelled) return;
- setBankOptions([]);
- }
- }
- void loadBankOptions();
- return () => {
- cancelled = true;
- };
- }, [selectedChannel, applyDialogOpen]);
- useEffect(() => {
- if (!selectedSavedAccount) return;
- if (!isBankType(selectedChannel?.type || "") && !isBankTelegraphic) return;
- setBankUnameInput(selectedSavedAccount.bankUname || "");
- setBankCardNumInput(selectedSavedAccount.bankCardNum || "");
- setBankNameInput(selectedSavedAccount.bankName || "");
- setBankBranchNameInput(selectedSavedAccount.bankBranchName || "");
- setSwiftCodeInput(selectedSavedAccount.swiftCode || "");
- setCustomBankCodeInput(selectedSavedAccount.customBankCode || "");
- setBankAddrInput(selectedSavedAccount.bankAddr || "");
- }, [selectedSavedAccount, selectedChannel?.type, isBankTelegraphic]);
- function validate(): string | null {
- if (!selectedChannel) return "请选择提款通道";
- if (!/^[0-9]+([.][0-9]{1,2})?$/.test(amount.trim())) return "请输入正确的提款金额";
- const amountNum = Number(amount);
- if (!Number.isFinite(amountNum) || amountNum <= 0) return "提款金额必须大于 0";
- if (selectedChannel.minAmount > 0 && amountNum < selectedChannel.minAmount) {
- return `提款金额不能低于 ${selectedChannel.minAmount}`;
- }
- if (selectedChannel.maxAmount > 0 && amountNum > selectedChannel.maxAmount) {
- return `提款金额不能高于 ${selectedChannel.maxAmount}`;
- }
- if (isWalletType(selectedChannel.type) && !address.trim()) return "请填写提款地址";
- if (shouldRequireSavedAccount && filteredSavedAccounts.length === 0) {
- return "当前通道暂无可用收款信息,请更换通道或先补充收款信息";
- }
- if (shouldShowSavedAccountSelector && !selectedSavedId) return "请选择收款信息";
- if (isBankType(selectedChannel.type)) {
- if (!bankUnameInput.trim()) return "请输入户名";
- if (!bankCardNumInput.trim()) return "请输入银行卡号";
- if (!bankNameInput.trim()) return "请输入银行名称";
- if (!bankBranchNameInput.trim()) return "请输入支行名称";
- }
- if (isBankTelegraphic) {
- if (!bankUnameInput.trim()) return "请输入户名";
- if (!bankCardNumInput.trim()) return "请输入银行卡号";
- if (!bankNameInput.trim()) return "请输入银行名称";
- if (!swiftCodeInput.trim()) return "请输入Swift Code";
- if (!customBankCodeInput.trim()) return "请输入银行代码";
- if (!bankAddrInput.trim()) return "请输入银行地址";
- }
- if (isBankTelegraphic && !agencyNo.trim()) return "请填写 Account Agency NO";
- if (isBankTelegraphic && needCpf && !cpf.trim()) return "请填写 CPF";
- if (isCardType(selectedChannel.type)) {
- if (!cardUnameInput.trim()) return "请输入信用卡户名";
- if (!cardNumInput.trim()) return "请输入信用卡账户";
- if (!cardCvvInput.trim()) return "请输入CVV";
- if (!cardExpiryInput.trim()) return "请输入到期年份/月 份";
- }
- if (!agree) return "请先勾选并同意提款条款";
- if (!agreeExtra) return "请勾选第二条提款确认条款";
- return null;
- }
- async function doSubmit() {
- if (!selectedChannel) return;
- const amountNum = Number(amount);
- const payload: Record<string, unknown> = {
- payType: selectedChannel.code,
- amount: amountNum,
- currency: selectedChannel.type === "BANK_TELEGRAPHIC" ? "USD" : selectedChannel.currency,
- agree2: true,
- };
- if (selectedBankCode) payload.bankCode = selectedBankCode;
- if (address.trim()) payload.address = address.trim();
- if (selectedSavedAccount) {
- payload.id = selectedSavedAccount.id;
- payload.bankUname = selectedSavedAccount.bankUname;
- payload.bankCardNum = selectedSavedAccount.bankCardNum;
- payload.bankName = selectedSavedAccount.bankName;
- payload.bankBranchName = selectedSavedAccount.bankBranchName;
- payload.bankAddr = selectedSavedAccount.bankAddr;
- payload.swiftCode = selectedSavedAccount.swiftCode;
- payload.customBankCode = selectedSavedAccount.customBankCode;
- payload.addressName = selectedSavedAccount.addressName;
- payload.address = payload.address ?? selectedSavedAccount.address;
- payload.cvv = selectedSavedAccount.cvv;
- payload.expiryYearMonth = selectedSavedAccount.expiryYearMonth;
- }
- if (isBankType(selectedChannel.type)) {
- payload.bankUname = bankUnameInput.trim();
- payload.bankCardNum = bankCardNumInput.trim();
- payload.bankName = bankNameInput.trim();
- payload.bankBranchName = bankBranchNameInput.trim();
- }
- if (isBankTelegraphic) {
- payload.bankUname = bankUnameInput.trim();
- payload.bankCardNum = bankCardNumInput.trim();
- payload.bankName = bankNameInput.trim();
- payload.swiftCode = swiftCodeInput.trim();
- payload.customBankCode = customBankCodeInput.trim();
- payload.bankAddr = bankAddrInput.trim();
- payload.currency = telegraphicCurrency || "USD";
- payload.agencyNo = agencyNo.trim();
- if (needCpf) payload.cpf = cpf.trim();
- }
- if (isCardType(selectedChannel.type)) {
- payload.bankUname = cardUnameInput.trim();
- payload.bankCardNum = cardNumInput.trim();
- payload.cvv = cardCvvInput.trim();
- payload.expiryYearMonth = cardExpiryInput.trim();
- }
- setSubmitting(true);
- try {
- await submitWithdrawApply({
- requestUrl: selectedChannel.requestUrl,
- payload,
- });
- setResultDialog({
- open: true,
- status: "success",
- title: "提交成功",
- message: "提款申请已提交,请等待平台审核。",
- });
- resetApplyForm();
- } catch (e) {
- const err = e as Error;
- setResultDialog({
- open: true,
- status: "error",
- title: "提交失败",
- message: err.message || "提款申请失败,请稍后重试。",
- });
- } finally {
- setSubmitting(false);
- setConfirmOpen(false);
- }
- }
- const channelGroups = useMemo(() => {
- const groups: Record<string, WithdrawChannel[]> = {};
- for (const item of channels) {
- const key = channelGroupLabel(item);
- if (!groups[key]) groups[key] = [];
- groups[key].push(item);
- }
- return Object.entries(groups).sort((a, b) => groupOrder(a[0]) - groupOrder(b[0]));
- }, [channels]);
- return (
- <div className="page-shell page-shell-wide">
- <h1 className="font-serif text-2xl font-semibold text-[var(--navy)]">提款申请</h1>
- <p className="mt-2 text-sm text-[var(--muted)]">流程:选择通道 - 填写信息 - 确认提交</p>
- <section className="mt-6 rounded-2xl border border-[var(--border)] bg-[var(--card)] p-4">
- <h2 className="text-sm font-semibold text-[var(--navy)]">提款通道</h2>
- {channelsLoading ? <InlineLoading text="通道加载中..." className="mt-2" /> : null}
- {channelsError ? <p className="mt-2 text-sm text-rose-700">{channelsError}</p> : null}
- {!channelsLoading && !channelsError && channelGroups.length === 0 ? (
- <p className="mt-2 text-sm text-[var(--muted)]">暂无可用通道</p>
- ) : null}
- <div className="mt-3 space-y-3">
- {channelGroups.map(([group, items]) => (
- <div key={group} className="rounded-lg border border-[var(--border)] bg-white">
- <button
- type="button"
- onClick={() => setExpandedGroup((v) => (v === group ? "" : group))}
- className="flex w-full items-center gap-2 px-3 py-2 text-left text-base font-semibold text-[var(--navy)]"
- >
- <span
- className={`text-sm transition-transform duration-300 ${
- expandedGroup === group ? "rotate-0" : "-rotate-90"
- }`}
- >
- ▼
- </span>
- <span>{group}</span>
- </button>
- <div
- className={`grid overflow-hidden transition-all duration-300 ease-out ${
- expandedGroup === group ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0"
- }`}
- >
- <div className="min-h-0">
- <div className="overflow-x-auto border-t border-[var(--border)]">
- <table className="w-full min-w-[900px] text-sm">
- <thead className="bg-slate-100/70 text-[var(--navy)]">
- <tr>
- <th className="px-3 py-2 text-left font-semibold">付款方式</th>
- <th className="px-3 py-2 text-left font-semibold">描述</th>
- <th className="px-3 py-2 text-left font-semibold">金额范围</th>
- <th className="px-3 py-2 text-left font-semibold">处理时间</th>
- <th className="px-3 py-2 text-left font-semibold">费用</th>
- <th className="px-3 py-2 text-right font-semibold">操作</th>
- </tr>
- </thead>
- <tbody>
- {items.map((item) => (
- <tr key={item.id} className="border-t border-[var(--border)]">
- <td className="px-3 py-2">
- <div className="flex items-center gap-2">
- {item.icon ? (
- // eslint-disable-next-line @next/next/no-img-element
- <img
- src={item.icon}
- alt={item.name || item.code}
- className="h-7 w-7 rounded object-contain"
- />
- ) : (
- <span className="inline-block h-7 w-7 rounded bg-slate-100 text-center leading-7">
- -
- </span>
- )}
- <span>{item.name || item.code}</span>
- </div>
- </td>
- <td className="px-3 py-2 text-[var(--muted)]">{item.enName || item.name || "-"}</td>
- <td className="px-3 py-2">{formatAmountRange(item)}</td>
- <td className="px-3 py-2">{item.fundingTime || "1 hours"}</td>
- <td className="px-3 py-2">{formatFee(item)}</td>
- <td className="px-3 py-2 text-right">
- <button
- type="button"
- onClick={() => {
- setSelectedChannelId(item.id);
- setApplyDialogOpen(true);
- }}
- className={`ui-interactive-btn rounded border px-4 py-1 text-xs font-semibold ${
- selectedChannelId === item.id
- ? "border-[var(--navy)] bg-[var(--navy)] text-white"
- : "border-[var(--border)] bg-white text-[var(--navy)] hover:bg-slate-50"
- }`}
- >
- 选择
- </button>
- </td>
- </tr>
- ))}
- </tbody>
- </table>
- </div>
- {items.length === 0 ? <p className="px-3 py-3 text-sm text-[var(--muted)]">暂无通道</p> : null}
- </div>
- </div>
- </div>
- ))}
- </div>
- {selectedChannel ? (
- <p className="mt-3 text-sm text-emerald-700">
- 已选择通道:{selectedChannel.name || selectedChannel.enName || selectedChannel.code}
- </p>
- ) : null}
- </section>
- <p className="mt-8 text-center text-sm">
- <Link href="/account" className="text-[var(--accent)] hover:underline">
- 返回用户中心
- </Link>
- </p>
- {selectedChannel ? (
- <ModalShell open={confirmOpen} className="max-w-md" zIndexClassName="z-[70]">
- <div className="w-full rounded-2xl border border-[var(--border)] bg-[var(--card)] p-5 shadow-xl">
- <p className="text-base font-semibold text-[var(--navy)]">确认提交提款申请?</p>
- <div className="mt-3 space-y-1 text-sm text-[var(--muted)]">
- <p>提款通道:{selectedChannel.name || selectedChannel.enName || selectedChannel.code}</p>
- <p>
- 提款金额:{amount} {selectedChannel.currency || "USD"}
- </p>
- </div>
- <div className="mt-5 flex justify-end gap-2">
- <button
- type="button"
- onClick={() => setConfirmOpen(false)}
- className="rounded-lg border border-[var(--border)] px-4 py-2 text-sm"
- >
- 取消
- </button>
- <button
- type="button"
- onClick={() => void doSubmit()}
- className="rounded-lg bg-[var(--navy)] px-4 py-2 text-sm text-white"
- >
- 确认提交
- </button>
- </div>
- </div>
- </ModalShell>
- ) : null}
- {selectedChannel ? (
- <ModalShell open={applyDialogOpen} className="max-w-2xl" zIndexClassName="z-[60]">
- <section
- className="w-full rounded-2xl border border-[var(--border)] bg-[var(--card)] p-4 shadow-xl"
- >
- <div className="flex items-center justify-between">
- <h2 className="text-sm font-semibold text-[var(--navy)]">填写提款信息</h2>
- <button
- type="button"
- onClick={() => {
- resetApplyForm();
- setApplyDialogOpen(false);
- }}
- className="ui-interactive-btn rounded border border-[var(--border)] px-2 py-1 text-xs text-[var(--muted)]"
- >
- 关闭
- </button>
- </div>
- <div className="mt-3 rounded-lg border border-[var(--border)] bg-slate-50 px-3 py-2 text-sm text-[var(--navy)]">
- 钱包余额:
- <span className="ml-1 font-semibold tabular-nums">
- {walletBalanceLoading ? "加载中..." : `$${(walletBalance ?? 0).toFixed(2)}`}
- </span>
- </div>
- {selectedChannel.introduce || selectedChannel.enIntroduce ? (
- <div
- className="mt-3 rounded-lg border border-[var(--border)] bg-slate-50 p-3 text-sm leading-7 text-[var(--navy)]"
- dangerouslySetInnerHTML={{
- __html: sanitizeHtml(selectedChannel.introduce || selectedChannel.enIntroduce || ""),
- }}
- />
- ) : null}
- {bankOptions.length > 0 ? (
- <div className="mt-3">
- <label className="text-sm font-medium text-[var(--navy)]">银行通道</label>
- <select
- value={selectedBankCode}
- onChange={(e) => setSelectedBankCode(e.target.value)}
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
- >
- <option value="">请选择银行通道</option>
- {bankOptions.map((item) => (
- <option key={item.code} value={item.code}>
- {item.name || item.enName || item.code}
- </option>
- ))}
- </select>
- </div>
- ) : null}
- {shouldShowSavedAccountSelector ? (
- <div className="mt-3">
- <label className="text-sm font-medium text-[var(--navy)]">收款信息</label>
- <select
- value={selectedSavedId}
- onChange={(e) => setSelectedSavedId(e.target.value)}
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
- >
- <option value="">请选择收款信息</option>
- {filteredSavedAccounts.map((item) => (
- <option key={item.id} value={item.id} disabled={item.type === 4 && item.authStatus === 0}>
- {item.type === 4
- ? `${item.addressName || "-"} - ${item.address || "-"}`
- : `${item.bankName || "-"} - ${item.bankCardNum || "-"}`}
- </option>
- ))}
- </select>
- </div>
- ) : null}
- {isBankType(selectedChannel.type) ? (
- <div className="mt-3 grid gap-3 md:grid-cols-2">
- <div>
- <label className="text-sm font-medium text-[var(--navy)]">户名</label>
- <input
- value={bankUnameInput}
- onChange={(e) => setBankUnameInput(e.target.value)}
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
- />
- </div>
- <div>
- <label className="text-sm font-medium text-[var(--navy)]">银行卡号</label>
- <input
- value={bankCardNumInput}
- onChange={(e) => setBankCardNumInput(e.target.value)}
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
- />
- </div>
- <div>
- <label className="text-sm font-medium text-[var(--navy)]">银行名称</label>
- <input
- value={bankNameInput}
- onChange={(e) => setBankNameInput(e.target.value)}
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
- />
- </div>
- <div>
- <label className="text-sm font-medium text-[var(--navy)]">支行名称</label>
- <input
- value={bankBranchNameInput}
- onChange={(e) => setBankBranchNameInput(e.target.value)}
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
- />
- </div>
- </div>
- ) : null}
- {selectedChannel.type === "BANK_TELEGRAPHIC" ? (
- <div className="mt-3 grid gap-3 md:grid-cols-3">
- <div>
- <label className="text-sm font-medium text-[var(--navy)]">户名</label>
- <input
- value={bankUnameInput}
- onChange={(e) => setBankUnameInput(e.target.value)}
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
- />
- </div>
- <div>
- <label className="text-sm font-medium text-[var(--navy)]">银行卡号</label>
- <input
- value={bankCardNumInput}
- onChange={(e) => setBankCardNumInput(e.target.value)}
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
- />
- </div>
- <div>
- <label className="text-sm font-medium text-[var(--navy)]">银行名称</label>
- <input
- value={bankNameInput}
- onChange={(e) => setBankNameInput(e.target.value)}
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
- />
- </div>
- <div>
- <label className="text-sm font-medium text-[var(--navy)]">Swift Code</label>
- <input
- value={swiftCodeInput}
- onChange={(e) => setSwiftCodeInput(e.target.value)}
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
- />
- </div>
- <div>
- <label className="text-sm font-medium text-[var(--navy)]">银行代码</label>
- <input
- value={customBankCodeInput}
- onChange={(e) => setCustomBankCodeInput(e.target.value)}
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
- />
- </div>
- <div>
- <label className="text-sm font-medium text-[var(--navy)]">银行地址</label>
- <input
- value={bankAddrInput}
- onChange={(e) => setBankAddrInput(e.target.value)}
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
- />
- </div>
- </div>
- ) : null}
- {isCardType(selectedChannel.type) ? (
- <div className="mt-3 grid gap-3 md:grid-cols-2">
- <div>
- <label className="text-sm font-medium text-[var(--navy)]">信用卡户名</label>
- <input
- value={cardUnameInput}
- onChange={(e) => setCardUnameInput(e.target.value)}
- placeholder="John Doe"
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
- />
- </div>
- <div>
- <label className="text-sm font-medium text-[var(--navy)]">信用卡账户</label>
- <input
- value={cardNumInput}
- onChange={(e) => setCardNumInput(e.target.value)}
- placeholder="5188 5136 1855 2975"
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
- />
- </div>
- <div>
- <label className="text-sm font-medium text-[var(--navy)]">CVV</label>
- <input
- value={cardCvvInput}
- onChange={(e) => setCardCvvInput(e.target.value)}
- placeholder="123"
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
- />
- </div>
- <div>
- <label className="text-sm font-medium text-[var(--navy)]">到期年份/月 份</label>
- <input
- value={cardExpiryInput}
- onChange={(e) => setCardExpiryInput(e.target.value)}
- placeholder="30/09"
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
- />
- </div>
- </div>
- ) : null}
- {shouldRequireSavedAccount && !shouldShowSavedAccountSelector ? (
- <p className="mt-3 text-xs text-[var(--muted)]">
- 当前通道暂无可用收款信息,请更换通道或先补充收款信息。
- </p>
- ) : null}
- {savedAccountsError && shouldRequireSavedAccount ? (
- <p className="mt-1 text-xs text-[var(--muted)]">
- 收款信息加载失败,可先切换其他通道后重试。
- </p>
- ) : null}
- {isWalletType(selectedChannel.type) ? (
- <div className="mt-3">
- <label className="text-sm font-medium text-[var(--navy)]">提款地址</label>
- <input
- value={address}
- onChange={(e) => setAddress(e.target.value)}
- className="mt-1 w-full rounded-lg border border-[var(--border)] px-3 py-2 text-sm"
- />
- </div>
- ) : null}
- {isBankTelegraphic ? (
- <div className="mt-3 grid gap-3 md:grid-cols-2">
- <div>
- <label className="text-sm font-medium text-[var(--navy)]">货币类型</label>
- <select
- value={telegraphicCurrency}
- onChange={(e) => setTelegraphicCurrency(e.target.value)}
- className="mt-1 w-full rounded-lg border border-[var(--border)] bg-white px-3 py-2 text-sm"
- >
- <option value="USD">USD</option>
- </select>
- </div>
- <div>
- <label className="text-sm font-medium text-[var(--navy)]">金额</label>
- <input
- value={amount}
- onChange={(e) => setAmount(e.target.value)}
- className="mt-1 w-full rounded-lg border border-[var(--border)] px-3 py-2 text-sm"
- />
- </div>
- </div>
- ) : (
- <div className="mt-3">
- <label className="text-sm font-medium text-[var(--navy)]">
- 提款金额({selectedChannel.currency || "USD"})
- </label>
- <input
- value={amount}
- onChange={(e) => setAmount(e.target.value)}
- className="mt-1 w-full rounded-lg border border-[var(--border)] px-3 py-2 text-sm"
- />
- </div>
- )}
- {isBankTelegraphic ? (
- <div className="mt-3 grid gap-3 md:grid-cols-2">
- <div>
- <label className="text-sm font-medium text-[var(--navy)]">Account Agency NO</label>
- <input
- value={agencyNo}
- onChange={(e) => setAgencyNo(e.target.value)}
- className="mt-1 w-full rounded-lg border border-[var(--border)] px-3 py-2 text-sm"
- />
- </div>
- {needCpf ? (
- <div>
- <label className="text-sm font-medium text-[var(--navy)]">CPF</label>
- <input
- value={cpf}
- onChange={(e) => setCpf(e.target.value)}
- className="mt-1 w-full rounded-lg border border-[var(--border)] px-3 py-2 text-sm"
- />
- </div>
- ) : null}
- </div>
- ) : null}
- {
- <label className="mt-3 flex items-start gap-2 text-sm text-[var(--navy)]">
- <input
- type="checkbox"
- checked={agree}
- onChange={(e) => setAgree(e.target.checked)}
- className="mt-0.5"
- />
- <span>我已阅读并同意提款条款,知悉手续费与到账时间以平台审核为准。</span>
- </label>
- }
- {
- <label className="mt-2 flex items-start gap-2 text-sm text-[var(--navy)]">
- <input
- type="checkbox"
- checked={agreeExtra}
- onChange={(e) => setAgreeExtra(e.target.checked)}
- className="mt-0.5"
- />
- <span>* 我确认本次提款信息准确无误,并接受平台审核结果。</span>
- </label>
- }
- {
- <button
- type="button"
- disabled={submitting}
- onClick={() => {
- const msg = validate();
- if (msg) {
- setResultDialog({
- open: true,
- status: "error",
- title: "请检查输入信息",
- message: msg,
- });
- return;
- }
- setConfirmOpen(true);
- }}
- className="ui-interactive-btn mt-4 w-full rounded-full bg-[var(--navy)] py-3 text-sm font-semibold text-white disabled:opacity-60"
- >
- {submitting ? "提交中..." : "提交提款申请"}
- </button>
- }
- </section>
- </ModalShell>
- ) : null}
- <ModalShell open={resultDialog.open} className="max-w-md" zIndexClassName="z-[80]">
- <div className="w-full overflow-hidden rounded-2xl border border-[var(--border)] bg-[var(--card)] shadow-2xl">
- <div
- className={`px-5 py-4 ${
- resultDialog.status === "success"
- ? "bg-gradient-to-r from-emerald-500/10 to-emerald-400/5"
- : "bg-gradient-to-r from-rose-500/10 to-rose-400/5"
- }`}
- >
- <div className="flex items-center gap-3">
- <span
- className={`inline-flex h-8 w-8 items-center justify-center rounded-full text-base font-bold ${
- resultDialog.status === "success"
- ? "bg-emerald-100 text-emerald-700"
- : "bg-rose-100 text-rose-700"
- }`}
- >
- {resultDialog.status === "success" ? "✓" : "!"}
- </span>
- <p className="text-base font-semibold text-[var(--navy)]">{resultDialog.title}</p>
- </div>
- </div>
- <div className="px-5 py-4">
- <p className="text-sm leading-6 text-[var(--muted)]">{resultDialog.message}</p>
- <button
- type="button"
- onClick={() => setResultDialog((prev) => ({ ...prev, open: false }))}
- className={`ui-interactive-btn mt-5 w-full rounded-full py-2.5 text-sm font-semibold text-white ${
- resultDialog.status === "success"
- ? "bg-emerald-600 hover:bg-emerald-700"
- : "bg-[var(--navy)] hover:bg-[var(--navy-soft)]"
- }`}
- >
- 我知道了
- </button>
- </div>
- </div>
- </ModalShell>
- </div>
- );
- }
|